package org.certificatetransparency.ctlog.serialization;

import com.google.common.base.Preconditions;
import com.google.protobuf.ByteString;
import org.certificatetransparency.ctlog.proto.Ct;

import java.io.IOException;
import java.io.InputStream;

/**
 * Converting binary data to CT structures.
 */
public class Deserializer {
  /**
   * Parses a SignedCertificateTimestamp from binary encoding.
   * @param inputStream byte stream of binary encoding.
   * @return Built CT.SignedCertificateTimestamp
   * @throws SerializationException if the data stream is too short.
   */
  public static Ct.SignedCertificateTimestamp parseSCTFromBinary(InputStream inputStream) {
    Ct.SignedCertificateTimestamp.Builder sctBuilder = Ct.SignedCertificateTimestamp.newBuilder();

    int version = (int) readNumber(inputStream, 1 /* single byte */);
    if (version != Ct.Version.V1.getNumber()) {
      throw new SerializationException(String.format("Unknown version: %d", version));
    }
    sctBuilder.setVersion(Ct.Version.valueOf(version));

    byte[] keyId = readFixedLength(inputStream, CTConstants.KEY_ID_LENGTH);
    sctBuilder.setId(Ct.LogID.newBuilder().setKeyId(ByteString.copyFrom(keyId)).build());

    long timestamp = readNumber(inputStream, CTConstants.TIMESTAMP_LENGTH);
    sctBuilder.setTimestamp(timestamp);

    byte[] extensions = readVariableLength(inputStream, CTConstants.MAX_EXTENSIONS_LENGTH);
    sctBuilder.setExtensions(ByteString.copyFrom(extensions));

    sctBuilder.setSignature(parseDigitallySignedFromBinary(inputStream));
    return sctBuilder.build();
  }

  /**
   * Parses a Ct.DigitallySigned from binary encoding.
   * @param inputStream byte stream of binary encoding.
   * @return Built Ct.DigitallySigned
   * @throws SerializationException if the data stream is too short.
   */
  public static Ct.DigitallySigned parseDigitallySignedFromBinary(InputStream inputStream) {
    Ct.DigitallySigned.Builder builder = Ct.DigitallySigned.newBuilder();
    int hashAlgorithmByte = (int) readNumber(inputStream, 1 /* single byte */);
    Ct.DigitallySigned.HashAlgorithm hashAlgorithm =
        Ct.DigitallySigned.HashAlgorithm.valueOf(hashAlgorithmByte);
    if (hashAlgorithm == null) {
      throw new SerializationException(
          String.format("Unknown hash algorithm: %x", hashAlgorithmByte));
    }
    builder.setHashAlgorithm(hashAlgorithm);

    int signatureAlgorithmByte = (int) readNumber(inputStream, 1 /* single byte */);
    Ct.DigitallySigned.SignatureAlgorithm signatureAlgorithm =
        Ct.DigitallySigned.SignatureAlgorithm.valueOf(signatureAlgorithmByte);
    if (signatureAlgorithm == null) {
      throw new SerializationException(
          String.format("Unknown signature algorithm: %x", signatureAlgorithmByte));
    }
    builder.setSigAlgorithm(signatureAlgorithm);

    byte[] signature = readVariableLength(inputStream, CTConstants.MAX_SIGNATURE_LENGTH);
    builder.setSignature(ByteString.copyFrom(signature));

    return builder.build();
  }

  /**
   * Reads a variable-length byte array with a maximum length.
   * The length is read (based on the number of bytes needed to represent the max data length)
   * then the byte array itself.
   * @param inputStream byte stream of binary encoding.
   * @param maxDataLength Maximal data length.
   * @return read byte array.
   * @throws SerializationException if the data stream is too short.
   */
  static byte[] readVariableLength(InputStream inputStream, int maxDataLength) {
    int bytesForDataLength = bytesForDataLength(maxDataLength);
    long dataLength = readNumber(inputStream, bytesForDataLength);

    byte[] rawData = new byte[(int) dataLength];
    int bytesRead;
    try {
      bytesRead = inputStream.read(rawData);
    } catch (IOException e) {
      //Note: A finer-grained exception type should be thrown if the client
      // ever cares to handle transient I/O errors.
      throw new SerializationException("Error while reading variable-length data", e);
    }

    if (bytesRead != dataLength) {
      throw new SerializationException(String.format("Incomplete data. Expected %d bytes, had %d.",
          dataLength, bytesRead));
    }

    return rawData;
  }

  /**
   * Reads a fixed-length byte array.
   * @param inputStream byte stream of binary encoding.
   * @param dataLength exact data length.
   * @return read byte array.
   * @throws SerializationException if the data stream is too short.
   */
  static byte[] readFixedLength(InputStream inputStream, int dataLength) {
    byte[] toReturn = new byte[dataLength];
    try {
      int bytesRead = inputStream.read(toReturn);
      if (bytesRead < dataLength) {
        throw new SerializationException(
            String.format("Not enough bytes: Expected %d, got %d.", dataLength, bytesRead));
      }
      return toReturn;
    } catch (IOException e) {
      throw new SerializationException("Error while reading fixed-length buffer", e);
    }
  }

  /**
   * Calculates the number of bytes needed to hold the given number:
   * ceil(log2(maxDataLength)) / 8
   * @param maxDataLength
   * @return
   */
  public static int bytesForDataLength(int maxDataLength) {
    return (int) (Math.ceil(Math.log(maxDataLength) / Math.log(2)) / 8);
  }

  /**
   * Read a number of numBytes bytes (Assuming MSB first).
   * @param inputStream byte stream of binary encoding.
   * @param numBytes exact number of bytes representing this number.
   * @return a number of at most 2^numBytes
   */
  static long readNumber(InputStream inputStream, int numBytes) {
    Preconditions.checkArgument(numBytes <= 8, "Could not read a number of more than 8 bytes.");

    long toReturn = 0;
    try {
      for (int i = 0; i < numBytes; i++) {
        int valRead = inputStream.read();
        if (valRead < 0) {
          throw new SerializationException(
              String.format("Missing length bytes: Expected %d, got %d.", numBytes, i));
        }
        toReturn = (toReturn <<  8) | valRead;
      }
      return toReturn;
    } catch (IOException e) {
      throw new SerializationException("IO Error when reading number", e);
    }
  }
}
